Skip to content

feat(MockUI): resize all GUI elements for 800×480 touch display#18

Merged
k9ert merged 29 commits intok9ert:mainfrom
maggo83:Resize-all-GUI-elements
Mar 6, 2026
Merged

feat(MockUI): resize all GUI elements for 800×480 touch display#18
k9ert merged 29 commits intok9ert:mainfrom
maggo83:Resize-all-GUI-elements

Conversation

@maggo83
Copy link
Copy Markdown
Collaborator

@maggo83 maggo83 commented Mar 3, 2026

Summary

Scales the MockUI to the full 800×480 resolution of the STM32F469 Discovery board.
All interactive elements have been resized for comfortable finger-touch use.

Changes

  • TitledScreen base class — shared base for all screens, unifying header/content layout
  • UI constants scaled for 800×480 — font sizes, padding, row heights, status bar proportions
  • Scroll/drag disabled — prevents accidental navigation on a capacitive touch display
  • Text fields, help modal, tour buttons scaled — consistent sizing across interactive controls
  • Helper button size increased — larger tap targets for menu item help buttons
  • Icons upgraded to 42×42px — btc_icons rebuilt from SVG via Inkscape at the new resolution
  • Lock screen PIN keypad resized — keypad grid and digit buttons scaled for 800×480

Testing

All 3 device integration tests pass end-to-end (build → flash → run):

  • test_i18n_repl_interface
  • test_i18n_files_on_flash
  • test_language_navigation_switch_persistence

The test infrastructure (conftest.py) was hardened: the device-responsive polling
logic was unified into a single _wait_for_device_responsive() helper with a
configurable upfront settle time after flash/reset. This fixes a race where the REPL
became available before NavigationController finished rendering, causing a GC cycle
to destroy the LVGL screen tree mid-test.

Known Issue / TODO

The LVGL built-in symbol icons (e.g. lv.SYMBOL.*) have not been resized to
match the 42×42px scale. This is intentional: these symbols will be replaced entirely
by the next BTCIcons release once it ships updated assets. Resizing the built-in font
now would be throw-away work.

maggo83 added 25 commits March 3, 2026 13:59
Add TitledScreen (titled_screen.py) as a shared base for every full-screen
view. It creates two fixed sub-containers:

  • title_bar – TITLE_ROW_HEIGHT px tall strip at the top, holding an
    optional back button (left) and a centred title label
  • body – fills all remaining height below the title bar

Add TITLE_ROW_HEIGHT = 60 to ui_consts.py (the only new constant needed;
TITLE_PADDING and BACK_BTN_* already existed).

Refactor GenericMenu, ActionScreen, InterfacesMenu and WalletMenu to
inherit from TitledScreen, removing their individual back-button /
title-label creation code. WalletMenu's editable name textarea and edit
button are now children of self.title_bar instead of the screen root.

Backward-compat aliases (self.title → self.title_lbl, self.container →
self.body) are preserved so call-sites outside these classes need no
changes.
Scale all size constants ~1.5–2× for better readability and touch targets
on the STM32F469 Discovery (800×480).

ui_consts.py
  • BTN_HEIGHT 50→75, BACK_BTN_HEIGHT/WIDTH 50/32→70/48
  • MENU_PCT 80→100 (fills full content area, no gap at bottom)
  • TITLE_PADDING 30→15 (tighter now that TITLE_ROW_HEIGHT gives fixed space)
  • STATUS_BTN_HEIGHT/WIDTH 30/40→50/60
  • SWITCH_HEIGHT/WIDTH 55/30→82/45
  • BTC_ICON_WIDTH 24→36, add BTC_ICON_ZOOM=384 (150%)
  • Symbol widths: 1-letter 11→16, 2-letter 19→28, 3-letter 27→40
  • Add STATUS_BAR_PCT=8, CONTENT_PCT=84
  • Add MENU_TITLE_FONT_SIZE=24, MENU_ITEM_FONT_SIZE=20

navigation_controller.py
  • Replace hardcoded height_pct=5 / lv.pct(90) with STATUS_BAR_PCT /
    CONTENT_PCT constants — device bar, wallet bar and content area now
    all driven from ui_consts.py

main_menu.py
  • Reduce promoted-item size multipliers to avoid oversized buttons at
    the new 75 px base: scan 2→1.3, SD load 2→1.3, smartcard import 3→2,
    add_wallet 2→1.3
Prevent the UI from being accidentally dragged/scrolled when content
fits within the screen:

  TitledScreen: set_scroll_dir(NONE) on both root and body
  NavigationController: set_scroll_dir(NONE) on self and content

Subclasses that genuinely need scrolling can re-enable it with
set_scroll_dir(lv.DIR.VER) after calling super().__init__().
- Set font_montserrat_22 and height=50 on wallet name textarea (generate seedphrase + manage wallet menus)
- Set font_montserrat_22 and height=50 on passphrase textarea
- Set font_montserrat_22 on help modal title, body text, and close button label
- Increase tour nav container height to 60, prev/next buttons to 60×50, skip button to 160×50, checkmark button to 60×50"
-In order to be easier to hit increased helper button size to full menu item height (no visible effect because transparent; considering context dependent resizing)
-increased width to be menu item height (no considering context depending height to have helper buttons horizontally aligned regardless of menu item heights)
Generator overhaul (tools/symbol_lib/generate_btc_icons.py):
- One .py per icon instead of a single 1.4MB monolith — fixes mpy-cross OOM
- Auto-detects SVG vs PNG input; SVG path renders via Inkscape into a
  fresh png-{size}/ sibling dir (always wiped, no stale renders)
- Bytes literals instead of bytes([...]) — literals land in flash ROM
  in frozen bytecode, zero heap at runtime; bytes([...]) caused an
  OOM crash at boot: 126 icons × 1764 bytes = 217KB on a 38KB heap
- Old helper scripts archived to tools/symbol_lib/archive/

Icon resolution: 24px → 42px from upstream SVGs via Inkscape
- BTC_ICON_ZOOM=256 (no runtime upscaling; bitmap is already target size)
- Boot heap: 172KB free (was 38KB with the old bytes([...]) approach)

Source assets removed from git tracking:
- tools/Bitcoin-Icons-0.1.10/ and data/ added to .gitignore
- Regenerate with: python3 tools/symbol_lib/generate_btc_icons.py \
      data/Bitcoin-Icons-0.1.10/svg/filled <symbol_lib_dir>
- PIN_BTN_WIDTH = 100px, PIN_BTN_HEIGHT = 75px (was 60×36)
- Row height: lv.SIZE_CONTENT (was hardcoded 48)
- Instruction and mask labels: width 320, font montserrat_22
- Digit labels: font montserrat_22
- Export PIN_BTN_WIDTH, PIN_BTN_HEIGHT from basic/__init__.py
…orage path

- ui_state.py: store ui_state_config.json on /flash/ (explicit absolute path)
- device_menu.py: add 'Restart Tour' menu item that immediately restarts the
  guided tour on the main menu without requiring a reboot; uses
  clear_history() + show_menu(None) + GuidedTour.start()
- ui_explainer.py: replace hardcoded 'Skip Tour' with i18n key TOUR_SKIP_BTN
- menu.py: replace hardcoded 'Close' in help popup with i18n key MODAL_CLOSE_BTN
- specter_ui_en.json: add DEVICE_MENU_RESTART_TOUR, TOUR_SKIP_BTN, MODAL_CLOSE_BTN
- specter_ui_de.json: add same keys (Tour neu starten, Tour beenden, Schließen)
  All new keys placed in their respective naming-convention groups.
…e GuidedTour

- navigation_controller.py: add INTRO_TOUR_STEPS class-level constant with
  static step definitions (element_spec, i18n_key, position); element_spec
  is None, a dotted attr-path string, or a (x, y, w, h) coordinate tuple.
  Add 'start_intro_tour' as a special case in show_menu: clears history,
  sets current_menu_id directly (no push), renders MainMenu, then starts
  tour overlay after refresh_ui. Update __init__ tour startup to use
  GuidedTour.resolve_steps().
- guided_tour.py: constructor now accepts pre-resolved steps as argument.
  start() simplified to reset index + show first step. Add staticmethod
  resolve_steps(static_steps, nav) that resolves None, (x,y,w,h) tuples
  (validated: 4 numeric values), and dotted str attr-paths via getattr
  chain; raises TypeError/ValueError on unexpected input.
- device_menu.py: remove GuidedTour import and _make_restart_tour_cb
  entirely. 'Restart Tour' menu item now uses 'start_intro_tour' string,
  consistent with all other navigation targets.
- conftest.py: rename click_button → click_by_label(delay); add
  click_by_index, find_labels_overlay, click_overlay_by_label,
  click_overlay_by_index, navigate_to_device_menu, _read_flash_json helpers
- test_i18n_device.py: update calls to renamed click_by_label
- test_tour_device.py: 9 ordered device tests covering tour auto-start,
  CLICKABLE-flag check on step-0 prev placeholder, nav buttons
  (prev/next/skip/checkmark), persistence across resets, restart from
  Device Menu; _tour_overlay_content_idx() shared helper avoids
  duplicating the dim-strip child-count query
…id constructor argument was stored as self.menu_id but never\nread anywhere. Navigation tracking is handled entirely via\nui_state.current_menu_id in the NavigationController.\n\n- Remove menu_id param from GenericMenu.__init__ and self.menu_id assignment\n- Remove self.menu_id = \"interfaces\" from InterfacesMenu\n- Update all GenericMenu() call sites (device, main, security, settings, seedphrase menus)"
Never populated at any call site - all menus are instantiated with
only a single parent argument. Cleaned up across the full chain:
TitledScreen, ActionScreen, GenericMenu, all subclasses (LockedMenu,
WalletMenu, BackupsMenu, FirmwareMenu, StorageMenu, ChangeWalletMenu,
PassphraseMenu, ConnectWalletsMenu, AddWalletMenu, GenerateSeedMenu),
and all factory functions (MainMenu, DeviceMenu, SecurityMenu,
SettingsMenu, SeedPhraseMenu).
- Convert factory functions to proper GenericMenu subclasses
- Add template method hooks to GenericMenu: TITLE_KEY class attribute,
  get_menu_items(t, state), post_init(t, state)
- Extract _build_menu_items() helper for LVGL widget construction
- Replace 15 boilerplate get_title() overrides with TITLE_KEY one-liner
- Remove 4 redundant get_menu_items() overrides returning []
- Remove self.container alias; use self.body everywhere
- Move parent/state/i18n wiring into TitledScreen so all screen types
  get it for free (not only GenericMenu subclasses)
- LockedMenu: migrate from GenericMenu to TitledScreen directly
  (it's a PIN entry screen, not a menu-with-items)
- InterfacesMenu: remove redundant on_back() override (inherited from TitledScreen)
- sync_i18n.py: add TITLE_KEY = "..." pattern to key scanner
- Convert factory functions to proper GenericMenu subclasses
- Add template method hooks to GenericMenu: TITLE_KEY class attribute,
  get_menu_items(t, state), post_init(t, state)
- Extract _build_menu_items() helper for LVGL widget construction
- Replace 15 boilerplate get_title() overrides with TITLE_KEY one-liner
- Remove 4 redundant get_menu_items() overrides returning []
- Remove self.container alias; use self.body everywhere
- Move parent/state/i18n wiring into TitledScreen so all screen types
  get it for free (not only GenericMenu subclasses)
- LockedMenu: migrate from GenericMenu to TitledScreen directly
  (it's a PIN entry screen, not a menu-with-items)
- InterfacesMenu: remove redundant on_back() override (inherited from TitledScreen)
- sync_i18n.py: add TITLE_KEY = "..." pattern to key scanner
- Remove dead SCRIPT=mock_ui.py default (file was deleted in PR#16)
- Set SCRIPT default to mockui_fw/main.py (single entry point for HW + sim)
- Add platform guard in mockui_fw/main.py: pyb import detects hardware
- Wire up ControlServer when --control flag is passed (simulator only)
- Add import sys to main.py (needed for argv check)
- Add build-i18n dependency to unix target (translation_keys.py must exist)
- Extract mockui-shared.py: platform-independent MockUI freeze, included by
  both mockui.py (HW) and unix.py (simulator)
- mockui.py: drop sim_control and demo scenarios (not for HW firmware)
- unix.py: sim_control is simulator-only, referenced via mockui-shared.py
- playground.py: drop sim_control, use mockui-shared.py
- Update mcp_server.py and sim_cli.py script path references
- Delete obsolete scenarios/main_bak.py
- Platform detection: use sys.platform ('linux'/'darwin' = simulator)
  instead of 'import pyb' which has a unix stub and always succeeds
- Mount build/flash_image as /flash before MockUI import so I18nManager
  finds lang_en.bin at /flash/i18n/ (same path as hardware)
- Fix sim_control freeze: use freeze('../scenarios', files) from parent
  dir to preserve 'sim_control' as package name (was stripped before)
- Remove redundant pyb.usb_mode('VCP') from main.py: boot.py already
  sets 'VCP+MSC' on hardware before main.py runs
Settings are accessible via the gear button in the device bar,
so the dedicated menu entry in the main menu is redundant.
Order is now: QR → Keyboard → SD → Flash.
Keyboard input is more commonly available than SD, so it belongs
higher in the list.
- Firmware version moved out of the body flex flow into a subtitle label
  anchored just below the title bar (uses TITLE_PADDING gap, doesn't
  push content down)
- Font size increased: instruction + PIN mask 22 → 28, fw version 12 → 16
- LOCKED_MENU_TITLE no longer embeds the version string; new separate
  key LOCKED_MENU_FW_VERSION holds the label prefix
- Shorten MENU_ENABLE_DISABLE_INTERFACES to fit narrow screens (EN + DE)
- Replace 'Wallet:' label + edit button with wider textarea (270px,
  font_28, white border) directly in the title bar
- Red trash button replaces the inline delete menu entry
- Custom keyboard maps (lower/upper/special) built at init time to
  avoid symbol lookups at import; cycle keys use LVGL-native strings
  'ABC'/'abc'/'1#'
- Keyboard parented to NavigationController (full 480x800) so it
  anchors to the true screen bottom; wallet bar hidden while open
- Correct lifecycle: keyboard created fresh in show_keyboard(),
  deleted in _close_keyboard(); EVENT.DELETE guard prevents leak on
  navigation-away
- Fix defocus-reverts-committed-name bug: original_name updated before
  _close_keyboard() so the DEFOCUSED event triggered by keyboard
  deletion is idempotent
- NavigationController.set_wallet_bar_visible() added to show/hide
  wallet bar (e.g. while keyboard is open)
The main menu was too long to fit on one screen. Store and Clear
targets are now each in their own sub-menu (StoreSeedphraseMenu,
ClearSeedphraseMenu), reached via 'Store to...' and 'Clear from...'
buttons. Routes store_seedphrase and clear_seedphrase added to
NavigationController. New SEEDPHRASE_MENU_STORAGE i18n key added (EN+DE).
Split the flat DeviceMenu (10+ items) into focused submenus that each
fit on one screen:

- SettingsMenu (4 items): Security, Storage, Preferences, Language
  - Language shown as top-level direct action with current lang inline
    e.g. "Select Language (DE)" — prevents users getting stuck
- SecuritySettingsMenu: Security features, Interfaces, Firmware, Backups,
  Danger Zone, Wipe Device
- StorageMenu: Internal Flash, SmartCard, SD Card, Backups (dual-entry)
- PreferencesMenu: Display, Sounds, Restart Tour

Manage Backups appears in both Security and Storage menus (dual-entry)
since it logically belongs to both contexts.

Rename device_menu.py → security_settings_menu.py (SecuritySettingsMenu)
Rename security_menu.py → security_features_menu.py (SecurityFeaturesMenu)
Add preferences_menu.py (PreferencesMenu)
Add i18n keys: MENU_SETTINGS_SECURITY, MENU_MANAGE_PREFERENCES (EN + DE)
Copy link
Copy Markdown
Contributor

@al-munazzim al-munazzim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Icon Storage Review

The layout/refactoring work looks solid — TitledScreen base class, 800×480 scaling, scroll disable, test hardening — all good.

The icon approach needs rethinking though. The old single btc_icons.py (4,042 lines) was split into 126 separate Python files, one per icon. Each is 52 lines of raw byte literal for a 42×42 A8 bitmap. Total: ~6,500 lines across 126 files — more than before.

Problems

  1. 126 Python files for bitmap data. Each has the same boilerplate (import + Icon(pattern=(...))) wrapping 1,764 bytes of bitmap. Storing bitmaps as Python hex literals is ~4x size expansion.

  2. All 126 are eagerly imported in btc_icons.py — no lazy loading benefit from the split. More filesystem overhead, zero memory advantage.

  3. The zoom hack (BTC_ICON_ZOOM, scaling in add_to_parent) suggests icons were generated at one size then LVGL-scaled. If regenerating anyway, just render at target pixel size.

Better approaches (any of these)

  • LVGL icon font — BTCIcons already ships as a font. One .c font file replaces all 126 Python files, icons become string-concatenable (like lv.SYMBOL.*), and LVGL handles scaling natively.
  • Binary .bin assets loaded from flash/filesystem at runtime — no Python parsing overhead, trivial to regenerate at any size.
  • Single data module — at minimum, keep everything in one file with a dict lookup instead of 126 separate modules.

TL;DR

The refactoring and UI scaling are 👍. The icon storage is the weakest part — 126 individual Python files with raw byte arrays is the most expensive option in both storage and runtime. A proper LVGL icon font would be dramatically better.

@maggo83
Copy link
Copy Markdown
Collaborator Author

maggo83 commented Mar 5, 2026

Why individual files? -> to easily handle self-rendered files, + re-render only some icons more easily + later easily add a selection mechanism to only include the icons that are actually used in the GUI (with build-time tool generate_btc_icons. similar to how it works with the keys for i18n)
Actually the reason to break it into several files was due to limitations of the cross compiler because the single file we had initially was too large. This way (single files) heap/memory is not exceeded as each file can be handled separately.

format: the files contain binary literals and are frozen during build -> minimal flash footprint for used icons (unused icons can later be filtered out if problematic, but currently no issue); zero overhead in ram/heap. Used size for all icon is around 200kb, hard to make it less w/o leaving out the icons completely

.bin would not change much (except making the build and handling more complicated), the data is already maximally reduced.

So.... except the anyway planned possibility of filtering out unused icosn during build time I would not change that as I do not see the benefit.

Regarding zoom: Was an intermediate step to figure out the proper scaling of the icons. Once the proper scale was found, icons were re-rendered from SVG at correct size (42x42) are are now used w/o scaling/zoom (parameterized to be factor of 1). So: no effect, items are constructed at correct size. Zoom left in as an option should we change later again. Can be removed if needed, but currently I do not see it doing any harm.

The refactor commit (81baf67) moved menus around but device tests were
still navigating the old paths. This commit fixes all 5 failing tests.

Changes in conftest.py:
- navigate_to_settings_menu(): clicks the gear button by live widget-tree
  index instead of by text label (button is icon-only, no text)
- _find_settings_btn_index(): walks the live screen JSON to locate the
  settings button dynamically (root[0] = device_bar, [2] = right_container,
  [1] = settings_btn), so structural changes produce clear assertion errors
- navigate_to_language_menu(): uses click_by_partial_label() on the base
  MENU_LANGUAGE string to handle the dynamic '(EN)' suffix robustly
- navigate_to_device_menu(): now navigates Settings -> Security (old
  'Manage Device' entry no longer exists)
- navigate_to_preferences_menu(): new helper, Settings -> Preferences
- dismiss_tour_if_present(): skips the tour overlay if visible; called in
  _require_device after flash so non-tour tests start with a clean overlay
- click_by_partial_label(): new helper for substring label matching

Changes in test files:
- test_tour_device: test_f uses navigate_to_preferences_menu() because
  'Restart Tour' moved from SecuritySettingsMenu to PreferencesMenu
- test_help_icon_device: uses navigate_to_settings_menu() (gear click)
  instead of REPL call to force main-menu rebuild
- test_i18n_device: updated navigation path comment in docstring

All 13 device tests pass after full rebuild + flash.
Copy link
Copy Markdown
Contributor

@al-munazzim al-munazzim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second Pass — I was wrong about the icon split

After reading generate_btc_icons.py more carefully, I see the actual constraint I missed:

mpy-cross OOM. The old single btc_icons.py was 4,042 lines / ~220 KB of source. When mpy-cross compiles it to frozen bytecode for the STM32, that's a single compilation unit that needs to fit in memory. Splitting into 126 × ~52-line files keeps each compilation unit small enough to survive on the target.

The comment in format_icon_file() explains the other half: bytes literals (b"\x00...") are stored in flash (ROM) as frozen constants with zero heap allocation at import time. Using bytes([...]) would allocate on the heap — 126 icons × 1,764 bytes = ~217 KB heap, which would OOM the STM32.

So the split is actually driven by two real embedded constraints:

  1. mpy-cross compilation memory limits
  2. Frozen bytecode ROM vs heap tradeoff

That said, the icon font approach would still be better if feasible — one .c font file compiled into firmware sidesteps both issues and gives you string-concatenable symbols. But the current approach is defensible given the MicroPython toolchain constraints.

Correcting my earlier review: the 126-file split is not arbitrary — it's a pragmatic workaround for real embedded limitations. 👍

Rest of the PR (what I didn't cover first time)

TitledScreen — Clean base class. Good layout documentation in the ASCII diagram. The self.title = self.title_lbl backward-compat alias is a nice touch.

ui_consts.py — Well-organized with comments showing old values. BTC_ICON_ZOOM = 256 (= 100%, no actual scaling) makes sense now — it's a hook for future size changes without regenerating all icons.

conftest.py — The _wait_for_device_responsive() consolidation is solid. Unified polling with configurable settle time, eliminates the race condition where REPL was available before NavigationController finished rendering. The settle=45 for fresh flash is generous but safe.

generate_btc_icons.py — Well-structured generator: SVG auto-detection, Inkscape rendering, --aggregate-only mode for custom icons, progress output. The render_svgs function wisely wipes the output dir to prevent stale PNGs from previous size runs. Only suggestion: add a --dry-run flag to preview what would be generated without writing.

Tool reorganization — Moving helper scripts from symbol_lib/helper/ to tools/symbol_lib/archive/ is clean separation of build tooling from runtime code.

Refactor: restructure settings submenus for new display/menu size
@maggo83 maggo83 requested a review from al-munazzim March 6, 2026 06:19
maggo83 added 2 commits March 6, 2026 07:21
…d-helper-menu

Tests: add on-device tests for guided tour and help icon popup
fix: restore and fully wire simulate target for MockUI
@k9ert k9ert merged commit a445326 into k9ert:main Mar 6, 2026
1 check passed
@maggo83 maggo83 deleted the Resize-all-GUI-elements branch March 9, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants